<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="Qtzh0" id="Qtzh0"><span data-lake-id="uad25695b" id="uad25695b">典型回答</span></h1>
  <p data-lake-id="u3bbd3989" id="u3bbd3989"><span data-lake-id="u63f0b315" id="u63f0b315">在日常开发中，我们经常会用到时间，我们有很多办法在Java代码中获取时间。但是不同的方法获取到的时间的格式都不尽相同，这时候就需要一种格式化工具，把时间显示成我们需要的格式。</span></p>
  <p data-lake-id="ue8d4be24" id="ue8d4be24"><br></p>
  <p data-lake-id="u72319f0c" id="u72319f0c"><span data-lake-id="u3092ca04" id="u3092ca04">最常用的方法就是使用SimpleDateFormat类。这是一个看上去功能比较简单的类，但是，一旦使用不当也有可能导致很大的问题。</span></p>
  <p data-lake-id="u4765c7c3" id="u4765c7c3"><br></p>
  <p data-lake-id="u230da731" id="u230da731"><span data-lake-id="u75222396" id="u75222396">在阿里巴巴Java开发手册中，有如下明确规定：</span></p>
  <p data-lake-id="u5e39ec22" id="u5e39ec22"><br></p>
  <p data-lake-id="uc05f6a97" id="uc05f6a97"><img src="https://cdn.nlark.com/yuque/0/2023/png/5378072/1673071467790-da4c1bfd-4aee-4441-832a-efb0009d11c9.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_53%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u903485f7" id="u903485f7"><br></p>
  <p data-lake-id="u2f733ef8" id="u2f733ef8"><span data-lake-id="uc475c219" id="uc475c219">也就是说SimpleDateFormat是非线程安全的，所以在多线程场景中，不能使用SimpleDateFormat作为共享变量。</span></p>
  <p data-lake-id="u07e89f26" id="u07e89f26"><span data-lake-id="u4fbc2070" id="u4fbc2070">​</span><br></p>
  <p data-lake-id="ua8210bb8" id="ua8210bb8"><span data-lake-id="u40c99901" id="u40c99901">因为SimpleDateFormat中的format方法在执行过程中，会使用一个成员变量calendar来保存时间。</span></p>
  <p data-lake-id="ubbba4ca0" id="ubbba4ca0"><br></p>
  <p data-lake-id="u80cf8b9e" id="u80cf8b9e"><span data-lake-id="uf5076f68" id="uf5076f68">如果我们在声明SimpleDateFormat的时候，使用的是static定义的。那么这个SimpleDateFormat就是一个共享变量，随之，SimpleDateFormat中的calendar也就可以被多个线程访问到。</span></p>
  <p data-lake-id="u1b9d40e8" id="u1b9d40e8"><br></p>
  <p data-lake-id="u83c6c8fc" id="u83c6c8fc"><span data-lake-id="u91398791" id="u91398791">假设线程1刚刚执行完</span><code data-lake-id="u6c81fa30" id="u6c81fa30"><span data-lake-id="uaf4e7eed" id="uaf4e7eed">calendar.setTime</span></code><span data-lake-id="u98f4a933" id="u98f4a933">把时间设置成2018-11-11，还没等执行完，线程2又执行了</span><code data-lake-id="u358a2f5c" id="u358a2f5c"><span data-lake-id="u5b9d395c" id="u5b9d395c">calendar.setTime</span></code><span data-lake-id="u61ae1024" id="u61ae1024">把时间改成了2018-12-12。这时候线程1继续往下执行，拿到的</span><code data-lake-id="uec423a41" id="uec423a41"><span data-lake-id="u021c6abc" id="u021c6abc">calendar.getTime</span></code><span data-lake-id="u2bdb7afd" id="u2bdb7afd">得到的时间就是线程2改过之后的。</span></p>
  <p data-lake-id="ub540c195" id="ub540c195"><br></p>
  <p data-lake-id="uee940d67" id="uee940d67"><span data-lake-id="u6784a26e" id="u6784a26e">想要保证线程安全，要么就是不要把SDF设置成成员变量，只设置成局部变量就行了，要不然就是加锁避免并发，或者使用JDK 1.8中的</span><strong><span data-lake-id="ua52e6aa2" id="ua52e6aa2">DateTimeFormatter</span></strong></p>
  <h1 data-lake-id="kqhYP" id="kqhYP"><span data-lake-id="u3e8c8924" id="u3e8c8924">扩展知识</span></h1>
  <h2 data-lake-id="IZqWA" id="IZqWA"><span data-lake-id="ub3112126" id="ub3112126">SimpleDateFormat用法</span></h2>
  <p data-lake-id="u64aacc6a" id="u64aacc6a"><br></p>
  <p data-lake-id="u38182f2c" id="u38182f2c"><span data-lake-id="u1122f1ad" id="u1122f1ad">SimpleDateFormat是Java提供的一个格式化和解析日期的工具类。它允许进行格式化（日期 -&gt; 文本）、解析（文本 -&gt; 日期）和规范化。SimpleDateFormat 使得可以选择任何用户定义的日期-时间格式的模式。</span></p>
  <p data-lake-id="uf0a004be" id="uf0a004be"><br></p>
  <p data-lake-id="u781c6385" id="u781c6385"><span data-lake-id="u5f30def6" id="u5f30def6">在Java中，可以使用SimpleDateFormat的format方法，将一个Date类型转化成String类型，并且可以指定输出格式。</span></p>
  <p data-lake-id="u33950239" id="u33950239"><br></p>
  <pre lang="java"><code>
// Date转String
Date data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dataStr = sdf.format(data);
System.out.println(dataStr);
</code></pre>
  <p data-lake-id="u59e0be9e" id="u59e0be9e"><br></p>
  <p data-lake-id="ua79ee193" id="ua79ee193"><span data-lake-id="u82d77c87" id="u82d77c87">以上代码，转换的结果是：2018-11-25 13:00:00，日期和时间格式由"日期和时间模式"字符串指定。如果你想要转换成其他格式，只要指定不同的时间模式就行了。</span></p>
  <p data-lake-id="ub18fa93f" id="ub18fa93f"><br></p>
  <p data-lake-id="ua4c3abbc" id="ua4c3abbc"><span data-lake-id="u4b66fa4f" id="u4b66fa4f">在Java中，可以使用SimpleDateFormat的parse方法，将一个String类型转化成Date类型。</span></p>
  <p data-lake-id="u7ed2399a" id="u7ed2399a"><br></p>
  <pre lang="java"><code>
// String转Data
System.out.println(sdf.parse(dataStr));
</code></pre>
  <p data-lake-id="u511a3ba9" id="u511a3ba9"><br></p>
  <h3 data-lake-id="RCMxJ" id="RCMxJ"><span data-lake-id="ub2390a1a" id="ub2390a1a">日期和时间模式表达方法</span></h3>
  <p data-lake-id="u051efcfd" id="u051efcfd"><br></p>
  <p data-lake-id="ud943d98c" id="ud943d98c"><span data-lake-id="ua1b912dd" id="ua1b912dd">在使用SimpleDateFormat的时候，需要通过字母来描述时间元素，并组装成想要的日期和时间模式。常用的时间元素和字母的对应表如下：</span></p>
  <p data-lake-id="u08240e5d" id="u08240e5d"><br></p>
  <p data-lake-id="u53d34ca8" id="u53d34ca8"><img src="http://www.hollischuang.com/wp-content/uploads/2018/11/15431240092595.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_41%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"><span data-lake-id="u70a2c0a0" id="u70a2c0a0">​</span></p>
  <p data-lake-id="ubb7ee56c" id="ubb7ee56c"><br></p>
  <p data-lake-id="u86b8c3fb" id="u86b8c3fb"><span data-lake-id="u68b62492" id="u68b62492">模式字母通常是重复的，其数量确定其精确表示。如下表是常用的输出格式的表示方法。</span></p>
  <p data-lake-id="u7d3e15b0" id="u7d3e15b0"><br></p>
  <p data-lake-id="udc69f60f" id="udc69f60f"><img src="http://www.hollischuang.com/wp-content/uploads/2018/11/15431240361504.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_30%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"><span data-lake-id="u57f1a787" id="u57f1a787">​</span></p>
  <p data-lake-id="ud8e21200" id="ud8e21200"><br></p>
  <h3 data-lake-id="u0DpF" id="u0DpF"><span data-lake-id="ua06cc56c" id="ua06cc56c">输出不同时区的时间</span></h3>
  <p data-lake-id="u33584e6f" id="u33584e6f"><br></p>
  <p data-lake-id="ud96c789a" id="ud96c789a"><span data-lake-id="ub87981b7" id="ub87981b7">时区是地球上的区域使用同一个时间定义。以前，人们通过观察太阳的位置（时角）决定时间，这就使得不同经度的地方的时间有所不同（地方时）。1863年，首次使用时区的概念。时区通过设立一个区域的标准时间部分地解决了这个问题。</span></p>
  <p data-lake-id="u17a2a146" id="u17a2a146"><br></p>
  <p data-lake-id="u3ad8a603" id="u3ad8a603"><span data-lake-id="ubc2933b2" id="ubc2933b2">世界各个国家位于地球不同位置上，因此不同国家，特别是东西跨度大的国家日出、日落时间必定有所偏差。这些偏差就是所谓的时差。</span></p>
  <p data-lake-id="u0d18b18b" id="u0d18b18b"><br></p>
  <p data-lake-id="u97ce2917" id="u97ce2917"><span data-lake-id="u5d55c5a8" id="u5d55c5a8">现今全球共分为24个时区。由于使用上常常1个国家，或1个省份同时跨着2个或更多时区，为了照顾到行政上的方便，常将1个国家或1个省份划在一起。所以时区并不严格按南北直线来划分，而是按自然条件来划分。例如，中国幅员宽广，差不多跨5个时区，但为了使用方便简单，实际上在只用东八时区的标准时即北京时间为准。</span></p>
  <p data-lake-id="uadd498cb" id="uadd498cb"><br></p>
  <p data-lake-id="u1d0e0932" id="u1d0e0932"><span data-lake-id="uc08260fc" id="uc08260fc">由于不同的时区的时间是不一样的，甚至同一个国家的不同城市时间都可能不一样，所以，在Java中想要获取时间的时候，要重点关注一下时区问题。</span></p>
  <p data-lake-id="u43ece2f8" id="u43ece2f8"><br></p>
  <p data-lake-id="u19f0a7e7" id="u19f0a7e7"><span data-lake-id="uc4a09d46" id="uc4a09d46">默认情况下，如果不指明，在创建日期的时候，会使用当前计算机所在的时区作为默认时区，这也是为什么我们通过只要使用</span><code data-lake-id="uc479f38d" id="uc479f38d"><span data-lake-id="u2d25770e" id="u2d25770e">new Date()</span></code><span data-lake-id="u66ed9c14" id="u66ed9c14">就可以获取中国的当前时间的原因。</span></p>
  <p data-lake-id="ue90dafea" id="ue90dafea"><br></p>
  <p data-lake-id="u13927c67" id="u13927c67"><span data-lake-id="ued539d17" id="ued539d17">那么，如何在Java代码中获取不同时区的时间呢？SimpleDateFormat可以实现这个功能。</span></p>
  <p data-lake-id="u16a6114e" id="u16a6114e"><br></p>
  <pre lang="java"><code>
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
System.out.println(sdf.format(Calendar.getInstance().getTime()));
</code></pre>
  <p data-lake-id="u27e25df9" id="u27e25df9"><br></p>
  <p data-lake-id="u8cc32597" id="u8cc32597"><span data-lake-id="u3b9916d9" id="u3b9916d9">以上代码，转换的结果是： 2018-11-24 21:00:00 。既中国的时间是11月25日的13点，而美国洛杉矶时间比中国北京时间慢了16个小时（这还和冬夏令时有关系，就不详细展开了）。</span></p>
  <p data-lake-id="ua101011f" id="ua101011f"><br></p>
  <blockquote data-lake-id="u20898ff7" id="u20898ff7">
   <p data-lake-id="u34b69c89" id="u34b69c89"><span data-lake-id="ud976185a" id="ud976185a">如果你感兴趣，你还可以尝试打印一下美国纽约时间（America/New_York）。纽约时间是2018-11-25 00:00:00。纽约时间比中国北京时间早了13个小时。</span></p>
  </blockquote>
  <p data-lake-id="u95f22908" id="u95f22908"><br></p>
  <p data-lake-id="u50aeeaa4" id="u50aeeaa4"><span data-lake-id="uc1c25b34" id="uc1c25b34">当然，这不是显示其他时区的唯一方法，不过本文主要为了介绍SimpleDateFormat，其他方法暂不介绍了。</span></p>
  <p data-lake-id="ue9b36fa5" id="ue9b36fa5"><br></p>
  <h2 data-lake-id="044e4ef0" id="044e4ef0"><span data-lake-id="ubdb045cb" id="ubdb045cb">SimpleDateFormat线程安全性</span></h2>
  <p data-lake-id="u6c86f3f9" id="u6c86f3f9"><br></p>
  <p data-lake-id="u2254b17d" id="u2254b17d"><span data-lake-id="uc46ab5e0" id="uc46ab5e0">由于SimpleDateFormat比较常用，而且在一般情况下，一个应用中的时间显示模式都是一样的，所以很多人愿意使用如下方式定义SimpleDateFormat：</span></p>
  <p data-lake-id="u225af09e" id="u225af09e"><br></p>
  <pre lang="java"><code>
public class Main {

    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        simpleDateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York"));
        System.out.println(simpleDateFormat.format(Calendar.getInstance().getTime()));
    }
}
</code></pre>
  <p data-lake-id="u9e0557de" id="u9e0557de"><br></p>
  <p data-lake-id="u0608871e" id="u0608871e"><strong><span data-lake-id="u80f8fccd" id="u80f8fccd">这种定义方式，存在很大的安全隐患。</span></strong></p>
  <p data-lake-id="ucb6faa7c" id="ucb6faa7c"><br></p>
  <h3 data-lake-id="Zv46S" id="Zv46S"><span data-lake-id="ucbf88f72" id="ucbf88f72">问题重现</span></h3>
  <p data-lake-id="u4f375267" id="u4f375267"><br></p>
  <p data-lake-id="u1c1c6d7e" id="u1c1c6d7e"><span data-lake-id="ud242668e" id="ud242668e">我们来看一段代码，以下代码使用线程池来执行时间输出。</span></p>
  <p data-lake-id="u445ed469" id="u445ed469"><br></p>
  <pre lang="java"><code>
   /** * @author Hollis */ 
   public class Main {

    /**
     * 定义一个全局的SimpleDateFormat
     */
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * 使用ThreadFactoryBuilder定义一个线程池
     */
    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();

    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue&lt;Runnable&gt;(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    /**
     * 定义一个CountDownLatch，保证所有子线程执行完之后主线程再执行
     */
    private static CountDownLatch countDownLatch = new CountDownLatch(100);

    public static void main(String[] args) {
        //定义一个线程安全的HashSet
        Set&lt;String&gt; dates = Collections.synchronizedSet(new HashSet&lt;String&gt;());
        for (int i = 0; i &lt; 100; i++) {
            //获取当前时间
            Calendar calendar = Calendar.getInstance();
            int finalI = i;
            pool.execute(() -&gt; {
                    //时间增加
                    calendar.add(Calendar.DATE, finalI);
                    //通过simpleDateFormat把时间转换成字符串
                    String dateString = simpleDateFormat.format(calendar.getTime());
                    //把字符串放入Set中
                    dates.add(dateString);
                    //countDown
                    countDownLatch.countDown();
            });
        }
        //阻塞，直到countDown数量为0
        countDownLatch.await();
        //输出去重后的时间个数
        System.out.println(dates.size());
    }
}
</code></pre>
  <p data-lake-id="u312d432c" id="u312d432c"><br></p>
  <p data-lake-id="ue11e8939" id="ue11e8939"><span data-lake-id="u3f284b3c" id="u3f284b3c">以上代码，其实比较简单，很容易理解。就是循环一百次，每次循环的时候都在当前时间基础上增加一个天数（这个天数随着循环次数而变化），然后把所有日期放入一个</span><strong><span data-lake-id="uea32961a" id="uea32961a">线程安全的</span></strong><span data-lake-id="u02d3ee57" id="u02d3ee57">、</span><strong><span data-lake-id="uc9c6f7f1" id="uc9c6f7f1">带有去重功能</span></strong><span data-lake-id="u5bdad293" id="u5bdad293">的Set中，然后输出Set中元素个数。</span></p>
  <p data-lake-id="ue30ba8fa" id="ue30ba8fa"><br></p>
  <blockquote data-lake-id="u789f96b6" id="u789f96b6">
   <p data-lake-id="u028b288e" id="u028b288e"><span data-lake-id="ua8f72124" id="ua8f72124">上面的例子我特意写的稍微复杂了一些，不过我几乎都加了注释。这里面涉及到了</span><span data-lake-id="uc064c53c" id="uc064c53c">线程池的创建</span><span data-lake-id="u4a32cc99" id="u4a32cc99">、</span><span data-lake-id="ud1e25ff0" id="ud1e25ff0">CountDownLatch</span><span data-lake-id="u1b5c1e0e" id="u1b5c1e0e">、lambda表达式、线程安全的HashSet等知识。感兴趣的朋友可以逐一了解一下。</span></p>
  </blockquote>
  <p data-lake-id="u0daced85" id="u0daced85"><br></p>
  <p data-lake-id="ud514732d" id="ud514732d"><span data-lake-id="u9053f3bc" id="u9053f3bc">正常情况下，以上代码输出结果应该是100。但是实际执行结果是一个小于100的数字。</span></p>
  <p data-lake-id="u0997133e" id="u0997133e"><br></p>
  <p data-lake-id="u6e16650b" id="u6e16650b"><span data-lake-id="u1e2d0bbe" id="u1e2d0bbe">原因就是因为SimpleDateFormat作为一个非线程安全的类，被当做了共享变量在多个线程中进行使用，这就出现了线程安全问题。</span></p>
  <p data-lake-id="uc8210046" id="uc8210046"><br></p>
  <p data-lake-id="u2c87a227" id="u2c87a227"><span data-lake-id="u1aff38c0" id="u1aff38c0">在阿里巴巴Java开发手册的第一章第六节——并发处理中关于这一点也有明确说明：</span></p>
  <p data-lake-id="u4f929114" id="u4f929114"><br></p>
  <p data-lake-id="u15e6142c" id="u15e6142c"><img src="http://www.hollischuang.com/wp-content/uploads/2018/11/guiyue2.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_54%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="ub7c9fc2b" id="ub7c9fc2b"><br></p>
  <p data-lake-id="ua7ed214e" id="ua7ed214e"><span data-lake-id="u4f2fbddc" id="u4f2fbddc">那么，接下来我们就来看下到底是为什么，以及该如何解决。</span></p>
  <p data-lake-id="u0b23e48c" id="u0b23e48c"><br></p>
  <h3 data-lake-id="YLeLt" id="YLeLt"><span data-lake-id="ua51f840a" id="ua51f840a">线程不安全原因</span></h3>
  <p data-lake-id="ubb146a6e" id="ubb146a6e"><br></p>
  <p data-lake-id="uaf6e2bb6" id="uaf6e2bb6"><span data-lake-id="ud5a14ec4" id="ud5a14ec4">通过以上代码，我们发现了在并发场景中使用SimpleDateFormat会有线程安全问题。其实，JDK文档中已经明确表明了SimpleDateFormat不应该用在多线程场景中：</span></p>
  <p data-lake-id="ubca62cc6" id="ubca62cc6"><br></p>
  <blockquote data-lake-id="u1e096aa0" id="u1e096aa0">
   <p data-lake-id="uac4af9e4" id="uac4af9e4"><span data-lake-id="uce06b4db" id="uce06b4db">Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.</span></p>
  </blockquote>
  <p data-lake-id="ub0124eba" id="ub0124eba"><br></p>
  <p data-lake-id="u88583d94" id="u88583d94"><span data-lake-id="u719744bc" id="u719744bc">那么接下来分析下为什么会出现这种问题，SimpleDateFormat底层到底是怎么实现的？</span></p>
  <p data-lake-id="u94943ea4" id="u94943ea4"><br></p>
  <p data-lake-id="ubb541b50" id="ubb541b50"><span data-lake-id="ueb0f006f" id="ueb0f006f">我们跟一下SimpleDateFormat类中format方法的实现其实就能发现端倪。</span></p>
  <p data-lake-id="ub25b226c" id="ub25b226c"><br></p>
  <p data-lake-id="uc95da906" id="uc95da906"><img src="http://www.hollischuang.com/wp-content/uploads/2018/11/15431313894397.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_22%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"><span data-lake-id="uaa628b09" id="uaa628b09">​</span></p>
  <p data-lake-id="uc097be44" id="uc097be44"><br></p>
  <p data-lake-id="u4b68e8d4" id="u4b68e8d4"><span data-lake-id="ubbd26fc6" id="ubbd26fc6">SimpleDateFormat中的format方法在执行过程中，会使用一个成员变量calendar来保存时间。这其实就是问题的关键。</span></p>
  <p data-lake-id="u8fd6a993" id="u8fd6a993"><br></p>
  <p data-lake-id="u482b1e2a" id="u482b1e2a"><span data-lake-id="ub6a81493" id="ub6a81493">由于我们在声明SimpleDateFormat的时候，使用的是static定义的。那么这个SimpleDateFormat就是一个共享变量，随之，SimpleDateFormat中的calendar也就可以被多个线程访问到。</span></p>
  <p data-lake-id="u1398b7bf" id="u1398b7bf"><br></p>
  <p data-lake-id="u91e932cf" id="u91e932cf"><span data-lake-id="ua0a4ecf1" id="ua0a4ecf1">假设线程1刚刚执行完</span><code data-lake-id="uc4dc457d" id="uc4dc457d"><span data-lake-id="u8a3a4f89" id="u8a3a4f89">calendar.setTime</span></code><span data-lake-id="u512d67c4" id="u512d67c4">把时间设置成2018-11-11，还没等执行完，线程2又执行了</span><code data-lake-id="u8f77df7c" id="u8f77df7c"><span data-lake-id="u9a9cd145" id="u9a9cd145">calendar.setTime</span></code><span data-lake-id="u4b0d8fc0" id="u4b0d8fc0">把时间改成了2018-12-12。这时候线程1继续往下执行，拿到的</span><code data-lake-id="u44c9013f" id="u44c9013f"><span data-lake-id="u1a063749" id="u1a063749">calendar.getTime</span></code><span data-lake-id="u86de0c5c" id="u86de0c5c">得到的时间就是线程2改过之后的。</span></p>
  <p data-lake-id="u60b22c4c" id="u60b22c4c"><br></p>
  <p data-lake-id="u57f365d5" id="u57f365d5"><span data-lake-id="u9acf0ddb" id="u9acf0ddb">除了format方法以外，SimpleDateFormat的parse方法也有同样的问题。</span></p>
  <p data-lake-id="u312d41b9" id="u312d41b9"><br></p>
  <p data-lake-id="u17a4eee7" id="u17a4eee7"><span data-lake-id="uef5d64ec" id="uef5d64ec">所以，不要把SimpleDateFormat作为一个共享变量使用。</span></p>
  <p data-lake-id="ub55554ff" id="ub55554ff"><br></p>
  <h3 data-lake-id="WLrIF" id="WLrIF"><span data-lake-id="ue02cdefd" id="ue02cdefd">如何解决</span></h3>
  <p data-lake-id="uc3daf946" id="uc3daf946"><br></p>
  <p data-lake-id="ua9aa05e3" id="ua9aa05e3"><span data-lake-id="ud526e5ed" id="ud526e5ed">前面介绍过了SimpleDateFormat存在的问题以及问题存在的原因，那么有什么办法解决这种问题呢？</span></p>
  <p data-lake-id="uaba93132" id="uaba93132"><br></p>
  <p data-lake-id="u60ea593c" id="u60ea593c"><span data-lake-id="u20f0e48c" id="u20f0e48c">解决方法有很多，这里介绍三个比较常用的方法。</span></p>
  <p data-lake-id="ufaa9dfd7" id="ufaa9dfd7"><br></p>
  <p data-lake-id="ufffbfd7f" id="ufffbfd7f"><strong><span data-lake-id="u42e00b61" id="u42e00b61">使用局部变量</span></strong></p>
  <p data-lake-id="ub5de304e" id="ub5de304e"><br></p>
  <pre lang="java"><code>
for (int i = 0; i &lt; 100; i++) {
    //获取当前时间
    Calendar calendar = Calendar.getInstance();
    int finalI = i;
    pool.execute(() -&gt; {
        // SimpleDateFormat声明成局部变量
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //时间增加
        calendar.add(Calendar.DATE, finalI);
        //通过simpleDateFormat把时间转换成字符串
        String dateString = simpleDateFormat.format(calendar.getTime());
        //把字符串放入Set中
        dates.add(dateString);
        //countDown
        countDownLatch.countDown();
    });
}
</code></pre>
  <p data-lake-id="u4d5b323e" id="u4d5b323e"><br></p>
  <p data-lake-id="u59bea7a8" id="u59bea7a8"><span data-lake-id="u176ce015" id="u176ce015">SimpleDateFormat变成了局部变量，就不会被多个线程同时访问到了，就避免了线程安全问题。</span></p>
  <p data-lake-id="u03e2bbdb" id="u03e2bbdb"><br></p>
  <p data-lake-id="u9e6e4598" id="u9e6e4598"><strong><span data-lake-id="ubbcee3a6" id="ubbcee3a6">加同步锁</span></strong></p>
  <p data-lake-id="ucc05bc89" id="ucc05bc89"><br></p>
  <p data-lake-id="u6795aff3" id="u6795aff3"><span data-lake-id="u174c61cb" id="u174c61cb">除了改成局部变量以外，还有一种方法大家可能比较熟悉的，就是对于共享变量进行加锁。</span></p>
  <p data-lake-id="u65072a2c" id="u65072a2c"><br></p>
  <pre lang="java"><code>
for (int i = 0; i &lt; 100; i++) {
    //获取当前时间
    Calendar calendar = Calendar.getInstance();
    int finalI = i;
    pool.execute(() -&gt; {
        //加锁
        synchronized (simpleDateFormat) {
            //时间增加
            calendar.add(Calendar.DATE, finalI);
            //通过simpleDateFormat把时间转换成字符串
            String dateString = simpleDateFormat.format(calendar.getTime());
            //把字符串放入Set中
            dates.add(dateString);
            //countDown
            countDownLatch.countDown();
        }
    });
}
</code></pre>
  <p data-lake-id="uf58a2466" id="uf58a2466"><br></p>
  <p data-lake-id="u743a8771" id="u743a8771"><span data-lake-id="u5cf57147" id="u5cf57147">通过加锁，使多个线程排队顺序执行。避免了并发导致的线程安全问题。</span></p>
  <p data-lake-id="u5e0fdbb6" id="u5e0fdbb6"><br></p>
  <p data-lake-id="u59473050" id="u59473050"><span data-lake-id="u890da694" id="u890da694">其实以上代码还有可以改进的地方，就是可以把锁的粒度再设置的小一点，可以只对</span><code data-lake-id="u87084a66" id="u87084a66"><span data-lake-id="u813a471f" id="u813a471f">simpleDateFormat.format</span></code><span data-lake-id="ua71c9ed3" id="ua71c9ed3">这一行加锁，这样效率更高一些。</span></p>
  <p data-lake-id="u30e17a1d" id="u30e17a1d"><br></p>
  <p data-lake-id="u403d2278" id="u403d2278"><strong><span data-lake-id="u9e0e636d" id="u9e0e636d">使用ThreadLocal</span></strong></p>
  <p data-lake-id="ucfc159ec" id="ucfc159ec"><br></p>
  <p data-lake-id="ua8f2d6d2" id="ua8f2d6d2"><span data-lake-id="ua385191d" id="ua385191d">第三种方式，就是使用 ThreadLocal。 ThreadLocal 可以确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象，那么自然也就不存在竞争问题了。</span></p>
  <p data-lake-id="ub945a47d" id="ub945a47d"><br></p>
  <pre lang="java"><code>
/**
 * 使用ThreadLocal定义一个全局的SimpleDateFormat
 */
private static ThreadLocal&lt;SimpleDateFormat&gt; simpleDateFormatThreadLocal = new ThreadLocal&lt;SimpleDateFormat&gt;() {
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
};

//用法
String dateString = simpleDateFormatThreadLocal.get().format(calendar.getTime());
</code></pre>
  <p data-lake-id="uc9f9b647" id="uc9f9b647"><br></p>
  <p data-lake-id="u1a3a088f" id="u1a3a088f"><span data-lake-id="ud24fac16" id="ud24fac16">用 ThreadLocal 来实现其实是有点类似于缓存的思路，每个线程都有一个独享的对象，避免了频繁创建对象，也避免了多线程的竞争。</span></p>
  <p data-lake-id="uc0fdf00c" id="uc0fdf00c"><br></p>
  <p data-lake-id="u0df10d8e" id="u0df10d8e"><span data-lake-id="u7d0f984c" id="u7d0f984c">当然，以上代码也有改进空间，就是，其实SimpleDateFormat的创建过程可以改为延迟加载。这里就不详细介绍了。</span></p>
  <p data-lake-id="u655e9389" id="u655e9389"><br></p>
  <p data-lake-id="ue58a1e26" id="ue58a1e26"><strong><span data-lake-id="ucd7f8bc1" id="ucd7f8bc1">使用DateTimeFormatter</span></strong></p>
  <p data-lake-id="u8f4fd62d" id="u8f4fd62d"><br></p>
  <p data-lake-id="ubc67c6e9" id="ubc67c6e9"><span data-lake-id="u818ce3a6" id="u818ce3a6">如果是Java8应用，可以使用DateTimeFormatter代替SimpleDateFormat，这是一个线程安全的格式化工具类。就像官方文档中说的，这个类 simple beautiful strong immutable thread-safe。</span></p>
  <p data-lake-id="u9bd78286" id="u9bd78286"><br></p>
  <pre lang="java"><code>
//解析日期
String dateStr= "2016年10月25日";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
LocalDate date= LocalDate.parse(dateStr, formatter);

//日期转换为字符串
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a");
String nowStr = now .format(format);
System.out.println(nowStr);
</code></pre>
  <p data-lake-id="uadc0d149" id="uadc0d149"><br></p>
 </body>
</html>